Phase 2a: Complete core extraction + integration test scaffolding (0.5.0)#9
Merged
Conversation
Enables integration testing against a real Rails::Application with real routing and request dispatch. Loads only action_controller/railtie — no ActiveRecord, no ActionMailer, no asset pipeline. Boots in under 100ms. Subsequent tasks add spec/rails_helper.rb and request specs that boot this dummy and dispatch HTTP requests at the mounted engine via Rack::Test.
rails_helper boots the spec/dummy/ app and provides Rack::Test for request dispatch. Unit specs continue to require "spec_helper" (no Rails boot); integration specs will require "rails_helper" starting with the next task's request specs. FakeConnectionHelper provides `stub_connection(tables:, columns_for:, exec_query:, select_value:)` for configuring the AR::Base.connection double each request spec needs. The helper builds a predictable double so specs don't have to repeat 20 lines of stub setup.
First integration spec. Verifies the root action returns 200, renders HTML with the expected tab markup, respects blocked_tables when filtering the table dropdown, and handles featured_tables. Boots spec/dummy/ Rails app via rails_helper and dispatches via Rack::Test. AR::Base.connection is stubbed via FakeConnectionHelper. Also fixes a boot-order bug in rails_helper: .rspec's --require spec_helper causes spec_helper to load mysql_genius before Rails is defined, so engine.rb's `if defined?(Rails)` guard skips loading MysqlGenius::Engine. By the time the dummy app boots, mysql_genius is already cached and require is a no-op — Engine is never defined. Fix: explicitly require "rails" and "mysql_genius/engine" at the top of rails_helper before booting the dummy app. Current 0.4.1 code passes all 4 examples — this spec documents existing behavior as a baseline for Phase 2a refactoring.
Six examples covering every branch of the columns action: - Happy path: JSON column metadata for a known table - Masked column filter: password/token columns excluded - default_columns config: :default flag correctly computed - Blocked table: 403 Forbidden with error message - Missing table: 404 Not Found with error message - Regression guard: does not raise NoMethodError (the 0.4.1 bug) The last example is the explicit pin: if someone in the future removes the masked_column? helper from QueriesController again, this spec fires before the code ships.
Covers happy path, SQL validation rejection (non-SELECT), blocked tables, and a regression guard for the 0.4.0 boot-order bug that prevented Core::Connection::ActiveRecordAdapter from being defined at runtime. Also fixes spec/dummy/config/boot.rb to set RAILS_ENV=test so that the test environment config (including allow_forgery_protection=false) is applied when running POST request specs under Rails 8.1+. Adds RSpec/VerifiedDoubleReference: Enabled: false to .rubocop.yml since ActiveRecord::Result is not loaded in the spec environment and string-form instance_double references are required.
One example per route plus a consolidated boot-order regression guard covering all six. Keeps the analysis logic unit-tested in core and the integration suite focused on routing/dispatch/JSON serialization concerns.
Covers every AI POST route with a stubbed Core::Ai::Client returning canned JSON. Focuses on routing/dispatch, config gating (ai not configured → 404), and input validation (blank params → 422). The actual prompt construction is tested in core once the AI builders are extracted in Stage B.
spec/regressions/phase_1b_latent_bugs_spec.rb holds explicit pins for both bugs so they can never silently come back: 1. Core::Connection::ActiveRecordAdapter is defined after \`require "mysql_genius"\` (0.4.0 boot-order bug, fixed in 3272a80). 2. GET /mysql_genius/columns does not raise NoMethodError on masked_column? for a valid non-blocked table (0.4.1 bug, fixed in 27d4662). Lives outside spec/requests/ so it's semantically clear these are regression pins, not functional behavior specs.
New optional keyword-init field, empty string default. Will be interpolated into the system prompts of the extracted AI builder classes (Stage B). Rails adapter will default it to the current "Ruby on Rails application, don't recommend FKs" string in Stage C; Phase 2b's sidecar will default to empty. Two new spec examples cover the default value and explicit assignment.
Shared helper for formatting table schema descriptions for AI prompt context. Takes a Core::Connection; #call(tables, detail:) returns a formatted multi-line string with table name, row count, primary key, columns, and indexes. detail: :with_cardinality adds information_schema.STATISTICS per-index cardinality. Consolidates the ~10 lines of schema description logic currently duplicated across 4 AI features in the Rails adapter's AiFeatures concern. SchemaReview, RewriteQuery, IndexAdvisor, and MigrationRisk (added in subsequent tasks) all compose this class.
Takes a Core::Connection plus config (blocked_tables, masked_column_patterns, default_columns) and returns a tagged Result struct with :ok / :blocked / :not_found status. Uses Core::SqlValidator.masked_column? for the masked-column filter. Matches the Phase 1b Core::Analysis::* pattern. The Rails adapter's QueriesController#columns action will be rewritten in Stage C to delegate to this class and map result.status to HTTP codes. Phase 2b's sidecar will call the same class with a TrilogyAdapter. The 0.4.1 masked_column? helper on QueriesController becomes unnecessary once this lands — deleted in Stage C.
Pure-function prompt builder for the "describe this SQL query" feature. Takes (client, config); #call(sql) returns whatever Core::Ai::Client#chat returns. Interpolates config.domain_context into the system prompt. First of 5 extracted AI prompt builders. The remaining 4 (SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk) follow the same pattern but take a Core::Connection for schema context lookup via SchemaContextBuilder.
Second of 5 extracted AI prompt builders. Reviews a specific table or the top 20 queryable tables for anti-patterns. Uses SchemaContextBuilder to format the schema description. Mirrors the pattern from DescribeQuery; adds a Core::Connection dependency.
Third of 5 extracted AI prompt builders. Extracts table references from the SQL via SqlValidator.extract_table_references, builds a SchemaContextBuilder for them, and sends the formatted schema + the SQL to Core::Ai::Client.
Fourth of 5 extracted AI prompt builders. Takes SQL + EXPLAIN rows and returns index recommendations. Uses SchemaContextBuilder with detail: :with_cardinality so the prompt includes information_schema. STATISTICS cardinality per index.
Fifth and final extracted AI prompt builder. Extracts table names from Rails migration helpers and raw SQL ALTER TABLE statements, pulls their schema via SchemaContextBuilder, and sends to the client. Completes Stage B's AI extraction. Stage C rewrites the Rails concern to delegate the 5 extracted actions to these classes. Concern's anomaly_detection and root_cause stay in place because they depend on the Redis-backed SlowQueryMonitor.
Returns the absolute path to the shared ERB template directory that Stage D will populate. Adapters register this path with their view loader. The directory does not exist yet — Stage D will create it and move the template files from app/views/mysql_genius/queries/ into it. This method just returns the well-formed string.
The action now builds an ActiveRecordAdapter and Columns service instance, calls it with the query params, and maps the tagged Result struct to HTTP status codes (:ok → 200, :blocked → 403, :not_found → 404). Response bodies are unchanged. The private masked_column?(name) helper added in the 0.4.1 hotfix is deleted — Core::Analysis::Columns handles masked-column filtering internally via Core::SqlValidator.masked_column?. The regression spec from Stage A guards the "does not raise NoMethodError" behavior at the HTTP layer. A new private rails_connection helper builds the ActiveRecordAdapter wrapper for reuse by subsequent Stage C tasks.
describe_query, schema_review, rewrite_query, index_advisor, and migration_risk are now thin delegators mirroring the Phase 1b pattern. Each action parses params, constructs the appropriate Core::Ai::* builder, and returns its JSON result. ai_domain_context private helper retained as a thin shim for anomaly_detection and root_cause (which stay inline due to Redis dependency); its Rails-FK string moves into the frozen RAILS_DOMAIN_CONTEXT constant passed to Core::Ai::Config as domain_context:. build_schema_for_query deleted — RewriteQuery uses SchemaContextBuilder directly. anomaly_detection and root_cause stay inline because they depend on the Redis-backed SlowQueryMonitor. No scope change.
Concern module providing path_for(name) and render_partial(name) as the 2-method contract the shared templates depend on. Both are exposed as helper_method so they work from ERB. QueriesController includes the concern. The engine registers Core.views_path as an additional app/views path so Rails finds the shared templates after they move in the next task. No template changes yet; suite stays green because no template actually calls path_for or render_partial.
Moves all 11 template files from app/views/mysql_genius/queries/ to gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/. Renames index.html.erb to dashboard.html.erb. Rewrites every mysql_genius.*_path (18 URL helpers) to path_for(:*) and every render "mysql_genius/queries/tab_*" (10 partial renders) to render_partial(:*), calling through the SharedViewHelpers contract. Rails::VERSION::MAJOR/MINOR references become @framework_version_major/minor instance variables populated by the controller's index action. QueriesController#index explicitly renders "mysql_genius/queries/dashboard" so Rails finds it under the engine's registered views_path. The engine registers Core.views_path before :add_view_paths (using before: ordering) so ActionController's on_load block picks up both view roots. render_partial delegates to view_context.render(partial:) to stay in ActionView's render pipeline and avoid DoubleRenderError.
- mysql_genius-core: 0.4.1 -> 0.5.0 - mysql_genius: 0.4.1 -> 0.5.0 - mysql_genius.gemspec dep: "~> 0.4.0" -> "~> 0.5.0" - CHANGELOG.md: new ## 0.5.0 section with Phase 2a additions - gems/mysql_genius-core/CHANGELOG.md: new ## 0.5.0 section The next commit tags v0.5.0 and kicks off the publish workflow, which will push mysql_genius-core 0.5.0 first and then mysql_genius 0.5.0 to rubygems.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes the core-library extraction work deferred from Phase 1b and adds integration test scaffolding that would have caught the two Phase 1b latent regressions. Zero observable behavior change for Rails host apps. Ships as
mysql_genius-core 0.5.0+mysql_genius 0.5.0in lockstep under onev0.5.0tag.See
docs/superpowers/specs/2026-04-11-phase-2a-core-extraction-design.mdfor the full design anddocs/superpowers/plans/2026-04-11-phase-2a-core-extraction.mdfor the 26-task implementation plan.What's included
Stage A — Integration test scaffolding (Tasks 1-9)
spec/dummy/minimal Rails engine dummy app (boots in <100ms, loads onlyaction_controller/railtie)spec/rails_helper.rb+spec/support/fake_connection.rbwithstub_connection/fake_columnhelpersspec/requests/mysql_genius/covering every route inconfig/routes.rb(40+ examples)spec/regressions/pinning the two Phase 1b latent bugsCLAUDE.mdrelaxed to a two-tier testing modelStage B — Core additions, no deletions (Tasks 10-18)
Core::Ai::Config#domain_contextfield (default empty string)Core::Ai::SchemaContextBuilder— shared helper for:basic/:with_cardinalityschema descriptionsCore::Analysis::Columns— service class with tagged:ok/:blocked/:not_foundresult structCore::Ai::{DescribeQuery, SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk}— 5 AI prompt builder classes extracted from the RailsAiFeaturesconcernMysqlGenius::Core.views_pathmodule method returning the shared templates directoryStage C — Rails adapter rewiring (Tasks 19-20)
QueriesController#columnsdelegates toCore::Analysis::Columns; 0.4.1masked_column?helper deletedCore::Ai::*classes;build_schema_for_queryhelper deleted;ai_domain_contextsimplifiedanomaly_detectionandroot_causestay Rails-side (Redis dependency)Stage D — ERB template migration (Tasks 21-22)
SharedViewHelpersconcern providingpath_for/render_partialviahelper_methodCore.views_pathvia initializerbefore: :add_view_pathsapp/views/mysql_genius/queries/togems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/index.html.erbrenamed todashboard.html.erbmysql_genius.*_path→path_for(:*)rewritesrender "..."→render_partial(:*)rewritesRails::VERSION::MAJOR/MINOR→@framework_version_major/minorivarsStage E — Release prep (Task 23)
0.5.0~> 0.4.0→~> 0.5.0Bugs caught during implementation
Stage A's tests-first approach found three Phase 1a/1b-class latent boot-order bugs before any refactoring began:
rails_helper.rbrequired explicitrequire "rails"; require "mysql_genius/engine"—.rspec's--require spec_helperauto-loadsspec_helperwhich requiresmysql_geniuswhen Rails isn't defined yet, solib/mysql_genius.rb'srequire "mysql_genius/engine" if defined?(Rails)silently skips andEngineis never defined. Caught in Task 3.spec/dummy/config/boot.rbneededENV["RAILS_ENV"] ||= "test"— Rails 8.1 defaults to development mode without this, and development-mode CSRF protection kicks in on POST requests causing 422 CSRF errors. Caught in Task 5..rubocop.ymlneededRSpec/VerifiedDoubleReference: Enabled: false— rubocop auto-correct was changinginstance_double("ActiveRecord::Result")to a constant-form reference, but the constant isn't autoloaded at spec time. Caught in Task 5.Plan vs reality — notable deviations
core/views/mysql_genius/queries/subdirectory (NOT flat atcore/views/) because Rails prepends the controller namespace during template lookup. Phase 2b's sidecarrender_partialwill need to handle this layout.before: :add_view_paths— Rails captures view paths during a specific phase.SharedViewHelpers#render_partialusesview_context.render(partial: ...)instead of plainrender(...)because the latter hits ActionController#render (full response cycle) not ActionView's partial renderer.ai_domain_contexthelper was NOT fully deleted — kept as a thin shim returningai_system_contextonly, becauseanomaly_detectionandroot_causestill reference it. The hardcoded Rails-FK guidance moved intoRAILS_DOMAIN_CONTEXTconstant used by the 5 extracted builders. Those 2 Rails-side actions drop the guidance from their prompts (acceptable — both are runtime diagnostics, not schema recommendations).git rebase --autosquashat the Task 9 checkpoint (my per-taskrubocop SPEC_FILEcalls only checked the new file each task touched).Test plan
Co-Authored-By: Claudetrailer in any commitepitomeGemfile at this branch's working tree, boot, exercise every tab (Dashboard, Slow Queries, Query Stats, Server, Tables, Unused Indexes, Duplicate Indexes, Query Explorer with a table selection, AI Tools), confirm zero regressions. This branch has 22 commits of Rails adapter + core gem changes, including the largest surface-area change of the project so far (the ERB migration).v0.5.0, monitor publish workflow, verify both gems at 0.5.0 on RubyGemsbundle installagainstgem "mysql_genius", "0.5.0"resolvesmysql_genius-core 0.5.0transitivelyWhat's next
gems/mysql_genius-desktop/Sinatra sidecar as the first non-Rails consumer ofmysql_genius-core 0.5.0. Will need to implement its ownpath_for/render_partialthat handle thecore/views/mysql_genius/queries/namespaced layout.